﻿package com.afw.papervision3d {
	import flash.display.DisplayObject;
	import flash.display.Stage;
	import flash.events.Event;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.utils.Dictionary;
	import org.papervision3d.materials.BitmapMaterial;
	import org.papervision3d.materials.MovieMaterial;
	import org.papervision3d.Papervision3D;
	import org.papervision3d.cameras.Camera3D;
	import org.papervision3d.render.BasicRenderEngine;
	import org.papervision3d.scenes.Scene3D;
	import org.papervision3d.view.Viewport3D;
	import org.papervision3d.objects.primitives.Plane;
	import org.papervision3d.objects.DisplayObject3D;
	import org.papervision3d.view.BasicView;
	import org.papervision3d.core.render.material.MaterialManager;
	import org.papervision3d.core.proto.MaterialObject3D;
	
	/**
	 * @description Papervision3D utility functions and lite framework.
	 * @usage Subclass this Class, (or not?)
	 * @version 0.2
	 * @author AllFlashWebsite.com
	 * @requirements The following libraries should be included in your classpath:
	 *	Papervision 3D v.2.0
	 */
	public class BasicViewPlus extends BasicView {
		protected var _renderPause:Boolean = false;
		
		/**
		 * Used in onViewportResize to calculate positions of static 3d containers
		 * corresponds to left of viewport
		 * TODO use getters instead of public vars?
		 */
		public var static3dLeft_X:Number;
		public var static3dRight_X:Number;
		
		/**
		 * Used in onViewportResize to calculate positions of static 3d containers
		 * corresponds to top of viewport
		 * TODO use getters instead of public vars?
		 */
		public var static3dTop_Y:Number;
		public var static3dBottom_Y:Number;
		
		public var static3dCenter_X:Number = 0;
		public var static3dCenter_Y:Number = 0;
		
		//public var planeClass:Class = Plane;
		private var static3dMainContainer:DisplayObject3D = new DisplayObject3D("static3dMainContainer");
		private var staticTL3dMainContainer:DisplayObject3D = new DisplayObject3D("staticTL3dMainContainer");
		//private var staticTC3dMainContainer:DisplayObject3D = new DisplayObject3D("staticTC3dMainContainer");
		
		private var freezeContainersMap:Dictionary = new Dictionary(true);
		private var freezeDO_count:int = 0;
		
		private var _staticDistanceCamera:Number = 1000;
		private var _staticZoom:Number = 2;
		private var _staticFocus:Number = _staticDistanceCamera / _staticZoom;
		
		/**
		 * Constructor
		 * 
		 * @param stage		The stage.
		 * @param width				Width (default=640).
		 * @param height			Height (default=480).
		 * @param scaleToStage
		 * @param interactive
		 * @param cameraType
		 */
		public function BasicViewPlus(viewportWidth:Number = 640, viewportHeight:Number = 480, scaleToStage:Boolean = true, interactive:Boolean = false, cameraType:String = "Target"):void {
			super(viewportWidth, viewportHeight, scaleToStage, interactive, cameraType);
			var camera:Camera3D = cameraAsCamera3D;//cameraAsFreeCamera3D;
			
			camera.z = -_staticDistanceCamera;
			validateStaticCamera();
			///////camera.fov = 50;
			//camera.zoom = 2;
			//camera.focus = _staticDistanceCamera / camera.zoom;
			
			scene.addChild(static3dMainContainer);
			static3dMainContainer.addChild(staticTL3dMainContainer);
			validateStatic3dContainer();
			
			if (scaleToStage)
				addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
			
			onViewportResize();
		}
		
		private function onAddedToStage(e:Event=null):void {
			removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
			stage.addEventListener(Event.RESIZE, onViewportResize);
		}
		
		/**
		 * Called during construction, setting viewportWidth/Height and every time the stage is resized if onAddedToStage == true
		 * NOTE: Don't resize the viewport directly, use the setters in this class instead
		 * @param	event	for creating event listeners
		 */
		protected function onViewportResize(event:Event = null):void {
			static3dRight_X = viewport.viewportWidth/2;
			static3dLeft_X = -static3dRight_X;
			static3dTop_Y = viewport.viewportHeight/2;
			static3dBottom_Y = -static3dTop_Y;
			
			staticTL3dMainContainer.x = static3dLeft_X;
			staticTL3dMainContainer.y = static3dTop_Y;
			
			dispatchEvent(new Event(Event.RESIZE));
		}
		
		/**
		 * Adjusts focus & zoom so static container is 1:1, assuming static container position is validated (see: validateStatic3dContainer)
		 */
		public function validateStaticCamera():void {
			camera.zoom = _staticZoom;
			camera.focus = _staticFocus;
		}
		
		/**
		 * Validates Static Camera: Call this function every time the camera moves position or rotates
		 * TODO: rename to validateStatic3dContainer?
		 */
		public function validateStatic3dContainer():void {
			static3dMainContainer.copyTransform(cameraAsCamera3D.transform);
			static3dMainContainer.moveForward( _staticDistanceCamera ); // positive number
		}
		
		/**
		 * Add a child to the static3dMainContainer
		 * @param	child
		 */
		public function addChildToStatic3dContainer(child:DisplayObject3D):DisplayObject3D {
			return static3dMainContainer.addChild(child);
		}
		
		/**
		 * Remove a child from the static3dMainContainer
		 * @param	child
		 * @return
		 */
		public function removeChildFromStatic3dContainer(child:DisplayObject3D):DisplayObject3D {
			return static3dMainContainer.removeChild(child);
		}
		
		/**
		 * The main container (top-level) for all static and frozen 3d objects
		 */
		public function get static3dContainer():DisplayObject3D {
			return static3dMainContainer;
		}
		
		/**
		 * Render DisplayObject to a plane in 3d space that does not appear to move and corresponds to original 2d space.
		 * The plane is placed inside a container that remains aligned with the flash TL 2d coordinate system.
		 * @param	source	DisplayObject, Sprite, MovieClip, etc...
		 * @param	switches	[SEE: attachDOtoPlane]
		 * @param	segmentsW	[SEE: attachDOtoPlane]
		 * @param	segmentsH	[SEE: attachDOtoPlane]
		 * @return new Plane. (remember: use plane.parent to get container)
		 */
		public function attachDOtoTLStatic3dPlane(source:DisplayObject, switches:Object = null, segmentsW:Number=4, segmentsH:Number=4, planeClass:Class=null, planeProps:Object=null):DisplayObject3D {
			var bounds:Rectangle = source.getBounds( viewport );
			
			var plane:DisplayObject3D = attachDOtoPlane(source, switches, segmentsW, segmentsH, planeClass, planeProps );
			
			//var newContainer:DisplayObject3D = do3dAlignTL3d(plane, source.width, source.height); // v.1 
			var newContainer:DisplayObject3D = do3dAlignTL3d(plane, bounds.width, bounds.height);  //v.2
			var newLocation:Point = TL2dtoTLStatic3d( new Point(bounds.x, bounds.y) );
			
			newContainer.x = newLocation.x;
			newContainer.y = newLocation.y;
			
			staticTL3dMainContainer.addChild(newContainer);
			
			return plane;
		}
		
		/**
		 * use this to reverse attachDOtoTLStatic3dPlane()
		 * WARNING: you should not have added any other children to the container (DO3D) of the plane
		 * @param	container Container or Plane
		 */
		public function removeTLStatic3dPlane(container:DisplayObject3D):void {
			//if (container is Plane || container is planeClass) container = container.parent as DisplayObject3D; // v.1
			if (container.numChildren == 0) container = container.parent as DisplayObject3D; // v.2
			
			for each (var o:DisplayObject3D in container.children) {
				container.removeChild(o);
				//MaterialManager.unRegisterMaterial(o.material);	// not necessary anymore?? // v.1
				o.material.destroy(); // v.2
			}
			
			staticTL3dMainContainer.removeChild(container);
			//trace("remove: " + container.numChildren);
		}
		
		/**
		 * Render DisplayObject to a plane in 3d space. TODO: MAKE STATIC
		 * @param	source	DisplayObject, Sprite, MovieClip, etc...
		 * @param	switches	material options. defaults: transparent=F, animated=F, precise=F, doubleSided=F,smooth=F,interactive=F,allowAutoResize=T, all other options also allowed
		 * @param	segmentsW
		 * @param	segmentsH
		 * @return new Plane.
		 */
		public static function attachDOtoPlane(source:DisplayObject, switches:Object = null, segmentsW:Number=4, segmentsH:Number=4, planeClass:Class=null, planeProps:Object=null ):DisplayObject3D {
			if (planeClass == null) planeClass = Plane;
			var initProps:Object = { transparent:false, animated:false, precise:false };
			var props:Object = { doubleSided:false, smooth:false, interactive:false, allowAutoResize:true };
			//var bounds:Rectangle = source.getBounds( viewport );
			var prop:String;
			
			for (prop in initProps) {
				if (switches && switches.hasOwnProperty(prop)) {
					initProps[prop] = switches[prop];
				}
			}
			
			var plane_mat:MovieMaterial = new MovieMaterial(source, initProps.transparent, initProps.animated, initProps.precise);
			
			// default props
			for (prop in props) {
				if (switches && switches.hasOwnProperty(prop)) {
					plane_mat[prop] = switches[prop];
				} else {
					plane_mat[prop] = props[prop];
				}
			}
			
			// additional props
			for (prop in switches) {
				if (! (initProps.hasOwnProperty(prop) || props.hasOwnProperty(prop))  ) {
					plane_mat[prop] = props[prop];
				}
			}
			
			//return new Plane(plane_mat, source.width, source.height, segmentsW, segmentsH);	// v.1
			var newPlane:DisplayObject3D =  new planeClass(plane_mat, source.width, source.height, segmentsW, segmentsH) as DisplayObject3D;
			for (prop in planeProps)
				newPlane[prop] = planeProps[prop];
			
			return newPlane;
		}
		
		/**
		 * Render BitmapData or DisplayObject to a plane in 3d space.
		 * @param	source	BitmapData, DisplayObject, Sprite, MovieClip, etc...
		 * @param	switches	material options. defaults: precise=F, doubleSided=F,smooth=F,interactive=F, all other options also allowed
		 * @param	segmentsW
		 * @param	segmentsH
		 * @return new Plane.
		 */
		public static function attachToPlane(source:*, switches:Object = null, segmentsW:Number=4, segmentsH:Number=4, planeClass:Class=null, planeProps:Object=null ):DisplayObject3D {
			if (source is DisplayObject) return attachDOtoPlane(source as DisplayObject, switches, segmentsW, segmentsH, planeClass, planeProps);
			
			if (planeClass == null) planeClass = Plane;
			var initProps:Object = { precise:false };
			var props:Object = { doubleSided:false, smooth:false, interactive:false };
			var prop:String;
			
			for (prop in initProps) {
				if (switches && switches.hasOwnProperty(prop)) {
					initProps[prop] = switches[prop];
				}
			}
			
			var plane_mat:BitmapMaterial = new BitmapMaterial(source, initProps.precise);
				
			// default props
			for (prop in props) {
				if (switches && switches.hasOwnProperty(prop)) {
					plane_mat[prop] = switches[prop];
				} else {
					plane_mat[prop] = props[prop];
				}
			}
			
			// additional props
			for (prop in switches) {
				if (! (initProps.hasOwnProperty(prop) || props.hasOwnProperty(prop))  ) {
					plane_mat[prop] = props[prop];
				}
			}
			
			var newPlane:DisplayObject3D =  new planeClass(plane_mat, source.width, source.height, segmentsW, segmentsH) as DisplayObject3D;
			for (prop in planeProps)
				newPlane[prop] = planeProps[prop];
			
			return newPlane;
		}
		
		/**
		 * Place a DisplayObject3D inside a new DisplayObject3D container, 
		 * and align the top-left corner of the do3d to the origin of the container;
		 * apply the plane's orignal transformation to the container.
		 * This essentially moves the pivot point from center to TL.
		 * @param	do3d	Plane or DisplayObject3D container a plane
		 * @param	w	width of do3d (or nested plane)
		 * @param	h	height of do3d (or nested plane)
		 * @return	a DisplayObject3D container with plane as child
		 */
		public static function do3dAlignTL3d(do3d:DisplayObject3D, w:Number, h:Number):DisplayObject3D {
			var cont:DisplayObject3D = new DisplayObject3D();
			cont.x = do3d.x - w/2;
			cont.y = do3d.y + h/2;
			cont.z = do3d.z;
			cont.addChild(do3d);
			do3d.x = w / 2;
			do3d.y = -h / 2;
			do3d.z = 0;
			return cont;
		}
		
		/**
		 * Place a DisplayObject3D inside a new DisplayObject3D container, 
		 * and align the top-center of the do3d to the origin of the container;
		 * apply the plane's orignal transformation to the container
		 * @param	do3d	Plane or DisplayObject3D container a plane	
		 * @param	w	width of do3d (or nested plane)
		 * @param	h	height of do3d (or nested plane)
		 * @return	a DisplayObject3D container with plane as child
		 */
		public static function do3dAlignTC3d(do3d:DisplayObject3D, w:Number, h:Number):DisplayObject3D {
			var cont:DisplayObject3D = new DisplayObject3D();
			cont.x = do3d.x;
			cont.y = do3d.y + h/2;
			cont.z = do3d.z;
			cont.addChild(do3d);
			do3d.y = -h / 2;
			do3d.z = 0;
			return cont;
		}
		
		 /**
		  *  Transforms (from Normal Flash Coordinate system) a TL2d point to a StaticTL3d point.
		  * @param	point in TL2d coord system
		  * @return point transformed to TLStatic3d coord system
		  */
		public static function TL2dtoTLStatic3d(point:Point):Point {
			return new Point(point.x, -point.y);
		}
		
		 /**
		  *  Transforms a TL3d point, to a StaticTL3d point.
		  * @param	point in TL3d coord system
		  * @return point transformed to TLStatic3d coord system
		  */
		public function TL3dtoTLStatic3d(point:Point):Point {
			return new Point(point.x + viewport.width / 2, point.y - viewport.height/2 );
		}
		
		 /**
		  *  Transforms a StaticTL3d point, to a TL3d point.
		  * @param	point in StaticTL3d coord system
		  * @return point transformed to TL3d coord system
		  */
		public function TLStatic3dtoTL3d(point:Point):Point {
			return new Point(point.x - viewport.width / 2, point.y + viewport.height/2 );
		}
		
		 /**
		  *  Transforms a TL2d point, to a TL3d point.
		  * @param	point in TL2d coord system
		  * @return point transformed to TL3d coord system
		  */
		public function TL2dtoTL3d(point:Point):Point {
			return new Point(point.x - viewport.width/2, -point.y + viewport.height/2 );
		}
		
		/**
		 * Hides a DisplayObject and renders it via the 3d viewport instead
		 * NOTE: source.getBounds() must return an accurate measurment (make sure source.width and source.height are correct)
		 * @param	source	Source Object
		 * @param	switches	[SEE: attachDOtoPlane]
		 * @param	segmentsW	[SEE: attachDOtoPlane]
		 * @param	segmentsH	[SEE: attachDOtoPlane]
		 * @returns	new plane (remember: use plane.parent to get container)
		 */
		public function freezeDO(source:DisplayObject, switches:Object = null, segmentsW:Number=4, segmentsH:Number=4, planeClass:Class=null, planeProps:Object=null ):DisplayObject3D {
			var plane:DisplayObject3D = attachDOtoTLStatic3dPlane( source, switches, segmentsW, segmentsH, planeClass, planeProps );
			
			plane.name = "freezeDO-plane-" + freezeDO_count;
			(plane.parent as DisplayObject3D).name = "freezeDO-cont-" + freezeDO_count++;
			
			//onRenderTick();
			source.visible = false;	// todo: add to a temporal enter_frame handler?
			freezeContainersMap[source] = plane.parent;
			return plane;
		}
		
		/**
		 * Unfreeze the specified DisplayObject
		 * @param	source	Source Object that freezeDO() has been called on
		 */
		public function unfreezeDO(source:DisplayObject):void {
			removeTLStatic3dPlane(freezeContainersMap[source] as DisplayObject3D);
			source.visible = true;
		}
		
		/**
		 * Get container of a frozen Plane (with DisplayObject source)
		 * @param	source	DisplayObject passed to corresponding freezeDO(...)
		 * @return	DisplayObject3D
		 */
		public function getFrozenDO3D(source:DisplayObject):DisplayObject3D {
			return (freezeContainersMap[source] as DisplayObject3D);
		}
		
		/**
		 * Paused?
		 */
		public function get renderPause():Boolean {
			return _renderPause;
		}
		
		/**
		 * Pause rendering (true) to save processor power
		 */
		public function set renderPause( value:Boolean ):void {
			if (value) stopRendering();
			else if (_renderPause) startRendering();
			_renderPause = value;
		}
		
		public function get staticDistanceCamera():Number {
			return _staticDistanceCamera;
		}
		
		/**
		 * Setting this value does not change the position of static3dContainer until validateStatic3dContainer() is called
		 * This determines the distance of static3dContainer from the camera.
		 */
		public function set staticDistanceCamera( value:Number ):void {
			_staticDistanceCamera = value;
		}
		
		public function get staticZoom():Number {
			return _staticZoom;
		}
		
		/**
		 * The camera does not actually change until you call validateStaticCamera().
		 * Changing staticZoom, automatically validates staticFocus and vice versa
		 */
		public function set staticZoom(v:Number):void {
			_staticZoom = v;
			_staticFocus = _staticDistanceCamera / _staticZoom;
		}
		
		public function get staticFocus():Number {
			return _staticFocus;
		}
		
		/**
		 * The camera does not actually change until you call validateStaticCamera().
		 * Changing staticZoom, automatically validates staticFocus and vice versa
		 */
		public function set staticFocus(v:Number):void {
			_staticFocus = v;
			_staticZoom = _staticDistanceCamera / _staticFocus;
		}
		
		override public function set viewportWidth(width:Number):void{
			super.viewportWidth = width;
			onViewportResize();
		}
		
		override public function set viewportHeight(height:Number):void {
			super.viewportHeight = height;
			onViewportResize();
		}

	}
}
